/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using DaveSexton.DocProject.MSBuild;
using System.IO;
using System.Windows.Forms.Design;
using DaveSexton.DocProject.Engine;
using System.Text.RegularExpressions;

namespace DaveSexton.DocProject.ExternalUI
{
	internal sealed class ProjectManager
	{
		#region Public Properties
		public MSBuildDocProject Project
		{
			get
			{
				return project;
			}
		}

		public IBuildEngine Engine
		{
			get
			{
				if (project == null)
					throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

				if (engine == null)
					engine = BuildController.GetEngineForProject(project);

				return engine;
			}
		}

		public bool IsDirty
		{
			get
			{
				return !project.IsSaved;
			}
			set
			{
				if (project == null)
					throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

				project.IsSaved = !value;

				OnIsDirtyChanged(new DocProjectEventArgs(project));
			}
		}

		public string Platform
		{
			get
			{
				if (project == null)
					throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

				return SelectedPlatform ?? project.Properties["Platform"];
			}
		}

		public string Configuration
		{
			get
			{
				if (project == null)
					throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

				return SelectedConfiguration ?? project.Properties["Configuration"];
			}
		}

		public IEnumerable<KeyValuePair<string, string>> ConfigurationPlatformPairs
		{
			get
			{
				if (project == null)
					throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

				foreach (Microsoft.Build.BuildEngine.BuildPropertyGroup group in project.Project.PropertyGroups)
				{
					string condition = group.Condition;

					if (!string.IsNullOrEmpty(condition))
					{
						foreach (Match match in Regex.Matches(condition, 
							@"^\s*" + Regex.Escape("'$(Configuration)|$(Platform)'") + @"\s*==\s*'(?<Configuration>.+?)\|(?<Platform>.+?)'\s*$",
							RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
						{
							string configuration = match.Groups["Configuration"].Value;
							string platform = match.Groups["Platform"].Value;

							yield return new KeyValuePair<string, string>(configuration, platform);
						}
					}
				}
			}
		}
		#endregion

		#region Private / Protected
		private string SelectedPlatform
		{
			get
			{
				return project.Settings.Read(SelectedPlatformKey);
			}
		}

		private string SelectedConfiguration
		{
			get
			{
				return project.Settings.Read(SelectedConfigurationKey);
			}
		}

		private const string SelectedPlatformKey = "DocProjectExternalUI.SelectedPlatform";
		private const string SelectedConfigurationKey = "DocProjectExternalUI.SelectedConfiguration";
		private const string openDialogFilter =
			"All DocProjects and DocSites (*.vbproj;*.csproj;*.vjsproj)|*.vbproj;*.csproj;*.vjsproj" +
			"|Visual Basic Projects (*.vbproj)|*.vbproj" +
			"|Visual C# Projects (*.csproj)|*.csproj" +
			"|Visual J# Projects (*.vjsproj)|*.vjsproj";
		private const string saveDialogFilter =
			"Visual Basic Projects (*.vbproj)|*.vbproj" +
			"|Visual C# Projects (*.csproj)|*.csproj" +
			"|Visual J# Projects (*.vjsproj)|*.vjsproj";

		private readonly IWin32Window owner;
		private readonly IUIService uiService;
		private readonly Microsoft.Build.BuildEngine.Engine msEngine;
		private IBuildEngine engine;
		private MSBuildDocProject project;
		private BuildContext buildContext;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="ProjectManager" /> class.
		/// </summary>
		public ProjectManager(IWin32Window owner, IServiceProvider services)
		{
			if (owner == null)
				throw new ArgumentNullException("owner");

			if (services == null)
				throw new ArgumentNullException("services");

			this.owner = owner;

			this.uiService = services.GetService(typeof(IUIService)) as IUIService;

			if (uiService == null)
				throw new InvalidOperationException(Resources.Errors.UIServiceRequired);

			msEngine = new Microsoft.Build.BuildEngine.Engine(
				System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory());
		}
		#endregion

		#region Methods
		public bool CloseProject(bool forceSave)
		{
			if (buildContext != null && !forceSave)
				throw new InvalidOperationException(Resources.Errors.BuildingProject);

			if (project == null)
				return true;
			else if (!project.IsSaved)
			{
				bool? result = SaveProject(!forceSave);

				if (!result.HasValue)
					// operation canceled by user
					return false;
			}

			CloseProject();

			return true;
		}

		private void CloseProject()
		{
			if (project == null)
				return;

			DocProjectEventArgs e = new DocProjectEventArgs(project);

			project = null;
			engine = null;

			OnProjectClosed(e);
		}

		private bool CanOpenProject()
		{
			if (buildContext != null)
				throw new InvalidOperationException(Resources.Errors.BuildingProject);

			if (project != null && !project.IsSaved)
			{
				bool? result = SaveProject(true);

				if (!result.HasValue)
					// operation canceled by user
					return false;
			}

			return true;
		}

		public bool OpenProject()
		{
			if (!CanOpenProject())
				return false;

			using (OpenFileDialog dialog = new OpenFileDialog())
			{
				dialog.CheckFileExists = true;
				dialog.CheckPathExists = true;
				dialog.Filter = openDialogFilter;
				dialog.ShowReadOnly = false;
				dialog.ShowHelp = false;
				dialog.Title = Resources.Text.OpenProjectDialogTitle;

				if (dialog.ShowDialog(owner) == DialogResult.OK)
				{
					CloseProject();
					OpenProject(dialog.FileName);
					return true;
				}
			}

			return false;
		}

		public bool OpenProject(string projectFile)
		{
			if (string.IsNullOrEmpty(projectFile))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "projectFile");

			if (!CanOpenProject())
				return false;

			try
			{
				project = new MSBuildDocProject(msEngine, projectFile);

				string config = SelectedConfiguration;

				if (!string.Equals(config, project.Properties["Configuration"], StringComparison.OrdinalIgnoreCase))
					SetConfiguration(config, false);

				string platform = SelectedPlatform;

				if (!string.Equals(platform, project.Properties["Platform"], StringComparison.OrdinalIgnoreCase))
					SetPlatform(platform, false);

				// TODO: project.ConfigurationProperties["SolutionDir"] = ...

				OnProjectOpened(new DocProjectEventArgs(project));

				return true;
			}
			catch (InvalidOperationException ex)
			{
				uiService.ShowError(ex);
			}
			catch (ArgumentException)
			{
				uiService.ShowError(string.Format(System.Globalization.CultureInfo.CurrentCulture, 
					Resources.Errors.ProjectFileNotDocProject, Environment.NewLine));
			}
			catch (IOException ex)
			{
				uiService.ShowError(ex);
			}

			return false;
		}

		public void SaveProject()
		{
			if (project == null)
				throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

			if (!project.IsSaved)
				SaveProject(false);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1300:SpecifyMessageBoxOptions")]
		private bool? SaveProject(bool askFirst)
		{
			if (askFirst)
			{
				switch (MessageBox.Show(owner, Resources.Text.AskSaveProject, Resources.Text.AskSaveProjectCaption, 
					MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1))
				{
					case DialogResult.No:
						return false;
					case DialogResult.Cancel:
						return null;
				}
			}

			project.Save();
			
			OnProjectSaved(new DocProjectEventArgs(project));

			return true;
		}

		public bool SaveProjectAs()
		{
			if (project == null)
				throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

			using (SaveFileDialog dialog = new SaveFileDialog())
			{
				dialog.AddExtension = true;
				dialog.CheckFileExists = false;
				dialog.CheckPathExists = true;
				dialog.CreatePrompt = true;
				dialog.DefaultExt = (project.IsVisualBasic)
					? ".vbproj" : (project.FilePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
						? ".csproj" : ".vjsproj";
				dialog.FileName = Path.GetFileName(project.FilePath);
				dialog.Filter = saveDialogFilter;
				dialog.FilterIndex = (project.IsVisualBasic)
					? 1	: (project.FilePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
						? 2 : 3;
				dialog.InitialDirectory = project.Directory;
				dialog.OverwritePrompt = true;
				dialog.ShowHelp = false;
				dialog.Title = Resources.Text.SaveProjectAsDialogTitle;

				if (dialog.ShowDialog(owner) == DialogResult.OK)
				{
					string file = dialog.FileName;

					project.SaveAs(file);

					OnProjectSaved(new DocProjectEventArgs(project));

					OpenProject(file);

					return true;
				}
			}

			return false;
		}

		public bool BuildProject(Microsoft.Build.Framework.ILogger logger, BuildTrace trace)
		{
			if (project == null)
				throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

			if (buildContext != null)
				throw new InvalidOperationException(Resources.Errors.BuildingProject);

			return BuildProjectInternal(logger, trace, false, BuildType.Default);
		}

		public bool RebuildProject(Microsoft.Build.Framework.ILogger logger, BuildTrace trace)
		{
			if (project == null)
				throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

			if (buildContext != null)
				throw new InvalidOperationException(Resources.Errors.BuildingProject);

			return BuildProjectInternal(logger, trace, true, BuildType.Full);
		}

		private bool BuildProjectInternal(Microsoft.Build.Framework.ILogger logger, BuildTrace trace, bool rebuild, BuildType helpBuildType)
		{
			PrepareForBuild(logger);

			Microsoft.Build.BuildEngine.BuildSettings settings;

			if (rebuild)
			{
				settings = Microsoft.Build.BuildEngine.BuildSettings.None;

				if (!project.Project.Build(new string[] { "Clean" }, null, settings))
					return false;
			}
			else
				settings = Microsoft.Build.BuildEngine.BuildSettings.DoNotResetPreviouslyBuiltTargets;

			if (!project.Project.Build(new string[] { "BuildBase" }, null, settings))
				return false;

			IBuildEngine buildEngine = Engine;
			bool success;

			try
			{
				buildContext = new BuildContext(buildEngine, trace);

				if (helpBuildType != BuildType.Default)
					buildEngine.BuildType = helpBuildType;

				buildEngine.Build(buildContext);

				success = buildContext.BuildState == BuildState.Successful;
			}
			finally
			{
				buildContext = null;
			}

			if (success && engine.IncludeBuildItems())
				IsDirty = true;

			OnBuildComplete(new DocProjectEventArgs(project));

			return success;
		}

		public bool CleanProject(Microsoft.Build.Framework.ILogger logger)
		{
			if (project == null)
				throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

			if (buildContext != null)
				throw new InvalidOperationException(Resources.Errors.BuildingProject);

			PrepareForBuild(logger);

			// The "Clean" target will automatically clean the DocProject as well (DaveSexton.DocProject.targets overrides CleanDependsOn)
			return project.Project.Build(new string[] { "Clean" }, null, Microsoft.Build.BuildEngine.BuildSettings.None);
		}

		private void PrepareForBuild(Microsoft.Build.Framework.ILogger logger)
		{
			if (!project.IsSaved)
				SaveProject();

			Microsoft.Build.BuildEngine.Engine parentEngine = project.Project.ParentEngine;

			parentEngine.UnregisterAllLoggers();

			if (logger != null)
				parentEngine.RegisterLogger(logger);
		}

		public void CancelBuild()
		{
			if (project == null)
				throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

			if (buildContext != null)
				buildContext.Cancel();
		}

		public void SetPlatform(string platform)
		{
			SetPlatform(platform, true);
		}

		private void SetPlatform(string platform, bool updateDirtyFlag)
		{
			if (project == null)
				throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

			if (buildContext != null)
				throw new InvalidOperationException(Resources.Errors.BuildingProject);

			bool hasPlatform = !string.IsNullOrEmpty(platform);

			if (hasPlatform)
			{
				project.ConfigurationProperties["Platform"] = platform;

				project.Settings[SelectedPlatformKey] = platform;

				if (updateDirtyFlag && project.Settings.IsDirty(SelectedPlatformKey))
					IsDirty = true;
			}
			else
			{
				project.ConfigurationProperties.Remove("Platform");

				if (project.Settings.IsCommitted(SelectedPlatformKey))
				{
					project.Settings.Delete(SelectedPlatformKey);

					if (updateDirtyFlag)
						IsDirty = true;
				}
			}

			foreach (MSBuildSourceProject source in project.ProjectSources)
			{
				if (hasPlatform)
					source.ConfigurationProperties["Platform"] = platform;
				else
					source.ConfigurationProperties.Remove("Platform");
			}

			System.Diagnostics.Debug.WriteLine("Set Global Platform Property: " + platform, "DocProject");
		}

		public void SetConfiguration(string configuration)
		{
			SetConfiguration(configuration, true);
		}

		private void SetConfiguration(string configuration, bool updateDirtyFlag)
		{
			if (project == null)
				throw new InvalidOperationException(Resources.Errors.ProjectNotLoaded);

			if (buildContext != null)
				throw new InvalidOperationException(Resources.Errors.BuildingProject);

			bool hasConfiguration = !string.IsNullOrEmpty(configuration);

			if (hasConfiguration)
			{
				project.ConfigurationProperties["Configuration"] = configuration;

				project.Settings[SelectedConfigurationKey] = configuration;

				if (updateDirtyFlag && project.Settings.IsDirty(SelectedConfigurationKey))
					IsDirty = true;
			}
			else
			{
				project.ConfigurationProperties.Remove("Configuration");

				if (project.Settings.IsCommitted(SelectedConfigurationKey))
				{
					project.Settings.Delete(SelectedConfigurationKey);

					if (updateDirtyFlag)
						IsDirty = true;
				}
			}

			foreach (MSBuildSourceProject source in project.ProjectSources)
			{
				if (hasConfiguration)
					source.ConfigurationProperties["Configuration"] = configuration;
				else
					source.ConfigurationProperties.Remove("Configuration");
			}

			System.Diagnostics.Debug.WriteLine("Set Global Configuration Property: " + configuration, "DocProject");
		}
		#endregion

		#region Events
		private readonly object ProjectClosedEventLock = new object();
		private EventHandler<DocProjectEventArgs> ProjectClosedEvent;
		private readonly object BuildCompleteEventLock = new object();
		private EventHandler<DocProjectEventArgs> BuildCompleteEvent;
		private readonly object IsDirtyChangedEventLock = new object();
		private EventHandler<DocProjectEventArgs> IsDirtyChangedEvent;
		private readonly object ProjectOpenedEventLock = new object();
		private EventHandler<DocProjectEventArgs> ProjectOpenedEvent;
		private readonly object ProjectSavedEventLock = new object();
		private EventHandler<DocProjectEventArgs> ProjectSavedEvent;

		/// <summary>
		/// Event raised when a DocProject or DocSite project is saved.
		/// </summary>
		public event EventHandler<DocProjectEventArgs> ProjectSaved
		{
			add
			{
				lock (ProjectSavedEventLock)
				{
					ProjectSavedEvent += value;
				}
			}
			remove
			{
				lock (ProjectSavedEventLock)
				{
					ProjectSavedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="ProjectSaved" /> event.
		/// </summary>
		/// <param name="e"><see cref="DocProjectEventArgs" /> object that provides the arguments for the event.</param>
		private void OnProjectSaved(DocProjectEventArgs e)
		{
			EventHandler<DocProjectEventArgs> handler = null;

			lock (ProjectSavedEventLock)
			{
				handler = ProjectSavedEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when a DocProject or DocSite project is first opened.
		/// </summary>
		public event EventHandler<DocProjectEventArgs> ProjectOpened
		{
			add
			{
				lock (ProjectOpenedEventLock)
				{
					ProjectOpenedEvent += value;
				}
			}
			remove
			{
				lock (ProjectOpenedEventLock)
				{
					ProjectOpenedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="ProjectOpened" /> event.
		/// </summary>
		/// <param name="e"><see cref="DocProjectEventArgs" /> object that provides the arguments for the event.</param>
		private void OnProjectOpened(DocProjectEventArgs e)
		{
			EventHandler<DocProjectEventArgs> handler = null;

			lock (ProjectOpenedEventLock)
			{
				handler = ProjectOpenedEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised after the <see cref="IsDirty" /> property value has changed.
		/// </summary>
		public event EventHandler<DocProjectEventArgs> IsDirtyChanged
		{
			add
			{
				lock (IsDirtyChangedEventLock)
				{
					IsDirtyChangedEvent += value;
				}
			}
			remove
			{
				lock (IsDirtyChangedEventLock)
				{
					IsDirtyChangedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="IsDirtyChanged" /> event.
		/// </summary>
		/// <param name="e"><see cref="EventArgs" /> object that provides the arguments for the event.</param>
		private void OnIsDirtyChanged(DocProjectEventArgs e)
		{
			EventHandler<DocProjectEventArgs> handler = null;

			lock (IsDirtyChangedEventLock)
			{
				handler = IsDirtyChangedEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when the build has completed.
		/// </summary>
		public event EventHandler<DocProjectEventArgs> BuildComplete
		{
			add
			{
				lock (BuildCompleteEventLock)
				{
					BuildCompleteEvent += value;
				}
			}
			remove
			{
				lock (BuildCompleteEventLock)
				{
					BuildCompleteEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="BuildComplete" /> event.
		/// </summary>
		/// <param name="e"><see cref="DocProjectEventArgs" /> object that provides the arguments for the event.</param>
		private void OnBuildComplete(DocProjectEventArgs e)
		{
			EventHandler<DocProjectEventArgs> handler = null;

			lock (BuildCompleteEventLock)
			{
				handler = BuildCompleteEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when the <see cref="Project"/> is closed.
		/// </summary>
		public event EventHandler<DocProjectEventArgs> ProjectClosed
		{
			add
			{
				lock (ProjectClosedEventLock)
				{
					ProjectClosedEvent += value;
				}
			}
			remove
			{
				lock (ProjectClosedEventLock)
				{
					ProjectClosedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="ProjectClosed" /> event.
		/// </summary>
		/// <param name="e"><see cref="ProjectEventArgs" /> object that provides the arguments for the event.</param>
		private void OnProjectClosed(DocProjectEventArgs e)
		{
			EventHandler<DocProjectEventArgs> handler = null;

			lock (ProjectClosedEventLock)
			{
				handler = ProjectClosedEvent;
			}

			if (handler != null)
				handler(this, e);
		}
		#endregion
	}
}
